/*
This file is part of IRCaBot.
IRCaBot is IRC logger with features.
Source code: https://notabug.org/acetone/ircabot.
Copyright (C) acetone, 2023.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#include "ircnetworkclient.h"
#include "currenttime.h"
#include "adminircpanel.h"
#include "voicegate.h"
#include "httpserver.h"
#include "voicedlist.h"
#include "ircloggerlist.h"

#include <QDateTime>
#include <QStringList>
#include <QRegularExpression>
#include <QDebug>
#include <QTimer>

#define LOG_WARN qWarning().noquote() << "[" + m_config->networkName + "]"
#define LOG_INFO qInfo().noquote() << "[" + m_config->networkName + "]"
#define LOG_DEBUG qDebug().noquote() << "[" + m_config->networkName + "]"

constexpr const int VOICEGATE_REQUEST_MINIMAL_INTERVAL = 120;
constexpr const int VOICEGATE_REQUEST_WITHOUT_ANSWER_THRESHOLD = 30;
constexpr const int VOICEGATE_SEND_INVITE_MINIMAL_INTERVAL = 12*60*60; // 12 hours
constexpr const int VOICEGATE_SEND_INVITE_DELAY = 5;
constexpr const int VOICEGATE_INITIALIZATION_DELAY = 5;

constexpr const char DIRECT_MESSAGE_TRIGGER_VOICEGATE[] = "voice";

IRCNetworkClient::IRCNetworkClient(QObject *parent) : QObject(parent) {}

IRCNetworkClient::~IRCNetworkClient()
{
    LOG_INFO << "Stopped";
}

void IRCNetworkClient::setConfig(QSharedPointer<Config::Network> config)
{
    if (config->instance != nullptr)
    {
        qFatal("IRCNetworkClient::setConfig receive config which initialized by another instanse");
    }
    config->instance = this;
    m_config = config;
    m_voicedList.setNetworkName(m_config->networkName);
}

uint IRCNetworkClient::reloadChannelList()
{
    uint changeCounter = 0;
    QMutexLocker lock (&m_mtxNetworkChannelsIteration);

    QStringList connectedChannelsTotalList;

    // PART
    QStringList channelsToPart;
    QListIterator<libircclient::Channel*> connectedChannelListIterator(m_network->GetChannels());
    while (connectedChannelListIterator.hasNext())
    {
        auto connectedChan = connectedChannelListIterator.next();
        if (not m_config->channels.contains(connectedChan->GetName()))
        {
            channelsToPart.push_back(connectedChan->GetName());
            changeCounter++;
        }
        connectedChannelsTotalList.push_back(connectedChan->GetName());
    }

    QStringListIterator channelsToPartIter (channelsToPart);
    while (channelsToPartIter.hasNext())
    {
        const auto chanName = channelsToPartIter.next();
        m_network->RequestPart(chanName);
        QThread::msleep(500);
    }

    // JOIN
    QStringList channelsToJoin;
    QStringListIterator newChannelListIter (m_config->channels);
    while (newChannelListIter.hasNext())
    {
        auto newChannel = newChannelListIter.next();
        if (not connectedChannelsTotalList.contains(newChannel))
        {
            channelsToJoin.push_back(newChannel);
            changeCounter++;
        }
    }

    QStringListIterator channelsToJoinIter (channelsToJoin);
    while (channelsToJoinIter.hasNext())
    {
        const auto chanName = channelsToJoinIter.next();
        m_network->RequestJoin(chanName);
        QThread::msleep(500);
    }

    return changeCounter;
}

void IRCNetworkClient::start()
{
    if (m_network != nullptr)
    {
        LOG_WARN << "IRCNetworkClient::start() called, but already started";
        return;
    }

    libirc::ServerAddress address(m_config->host);
    address.SetPort(m_config->port);
    address.SetSSL(m_config->ssl);
    address.SetNick(m_config->nickname);
    address.SetPassword(m_config->password);
    address.SetIdent(m_config->ident);
    address.SetRealname(m_config->realName);

    m_network = QSharedPointer<libircclient::Network> (new libircclient::Network(address, m_config->networkName));
    m_network->SetDefaultIdentifyString(m_config->identifyFormat);
    m_network->Connect();

    connect(m_network.data(), &libircclient::Network::Event_WhoisRegNick, this, &IRCNetworkClient::Slot_adminIdentified);
    connect(m_network.data(), &libircclient::Network::Event_PRIVMSG, this, &IRCNetworkClient::Slot_privMsg);

    connect(this, &IRCNetworkClient::Event_NewDirectMessage, this, &IRCNetworkClient::Slot_onDirectMessage);
    connect(this, &IRCNetworkClient::Event_NewChannelMessage, this, &IRCNetworkClient::Slot_onChannelMessage);

    connect(m_network.data(), &libircclient::Network::Event_Mode, this, &IRCNetworkClient::Slot_onMode);

    connect(m_network.data(), &libircclient::Network::Event_Part, this, [&](libircclient::Parser*, libircclient::Channel* c){
        LOG_INFO << "Out from" << c->GetName();
    });

    connect(m_network.data(), &libircclient::Network::Event_Connected, this, [&](){
        LOG_DEBUG << "Connected to server socket";
    });

    connect(m_network.data(), &libircclient::Network::Event_Welcome, this, &IRCNetworkClient::Slot_onWelcome);

    connect(m_network.data(), &libircclient::Network::Event_Disconnected, this, &IRCNetworkClient::Slot_onDisconnected);
    connect(m_network.data(), &libircclient::Network::Event_Join, this, &IRCNetworkClient::Slot_onJoin);

    connect(this, &IRCNetworkClient::doStopPrivate, this, &IRCNetworkClient::stopPrivate, Qt::QueuedConnection);
}

void IRCNetworkClient::stop()
{
    emit doStopPrivate();
}

void IRCNetworkClient::Slot_privMsg(libircclient::Parser *p)
{
    const QString receiver = p->GetParameterLine();
    const QString sender = p->GetSourceUserInfo()->GetNick();
    QString text = p->GetText().trimmed();

    if (text.isEmpty()) return;

    if (receiver == m_network->GetLocalUserInfo()->GetNick())
    {
        emit Event_NewDirectMessage(sender, text);
    }
    else
    {
        emit Event_NewChannelMessage(receiver, sender, text);
    }
}

void IRCNetworkClient::Slot_onDirectMessage(QString sender, QString text)
{
    if (sender == m_config->ircabotAdmin and text.startsWith("!"))
    {
        processAdminCommand(text);
    }

    else/* if (text.startsWith(DIRECT_MESSAGE_TRIGGER_VOICEGATE, Qt::CaseInsensitive))*/
    {
        processVoiceRequest(sender); // only one trigger
    }
}

void IRCNetworkClient::Slot_onChannelMessage(QString channel, QString sender, QString text)
{
    m_voicedList.updateActivity(sender);

    if (text.isEmpty())
    {
        LOG_WARN << "Received empty message in" << channel << "from" << sender;
        return;
    }

    if (m_config->bridgeNicknames.contains(sender, Qt::CaseInsensitive))
    {
        /*
         * Bridged message patterns:
         * <NICK> message
         * [NICK] message
         * NICK: message
         *
         * Bridged message sender log syntax:
         * BRIDGE_NICK~PARSED_SENDER_NICK
         */

        QString updatedText {text};
        int secondBrace = 0;
        if (updatedText.front() == '<')
        {
            updatedText.remove(0, 1);
            secondBrace = updatedText.indexOf('>');
        }
        else if (updatedText.front() == '[')
        {
            updatedText.remove(0, 1);
            secondBrace = updatedText.indexOf(']');
        }
        else
        {
            secondBrace = updatedText.indexOf(':');
            if (secondBrace+1 >= updatedText.size())
            {
                secondBrace = -1;
            }
        }

        if (secondBrace < 0)
        {
            LOG_WARN << "Can't process bridged message from" << sender+"." << "No known syntax in" << text;
        }
        else
        {
            QString updatedSender(updatedText);
            updatedSender = updatedSender.remove(secondBrace, updatedSender.size() - secondBrace).trimmed();
            updatedText = updatedText.remove(0, secondBrace+1).trimmed();
            LOG_DEBUG << "Bridged message from" << sender << "transformed with new sender" << updatedSender;
            text = updatedText;
            sender = sender + "~" + updatedSender;
        }
    }

    if (text.startsWith('.') and m_config->hideMessageByLeadingDot)
    {
        IRCLoggerList::get(m_config->networkName, channel)->addMessage("", "");
    }
    else
    {
        IRCLoggerList::get(m_config->networkName, channel)->addMessage(sender, text);
    }
}

void IRCNetworkClient::Slot_onJoin(libircclient::Parser *, libircclient::User *u, libircclient::Channel *c)
{
    if (u->GetNick() == m_network->GetLocalUserInfo()->GetNick())
    {
        LOG_INFO << "Joined to" << c->GetName();

        QTimer* timer = new QTimer;
        timer->setSingleShot(true);
        timer->setInterval(VOICEGATE_INITIALIZATION_DELAY*1000);
        connect (timer, &QTimer::timeout, this, [&, timer, c](){
            giveTheVoiceFlag(m_voicedList.nicks(), c->GetName());
            timer->deleteLater();
        });
        timer->start();

        return;
    }

    if (m_config->voiceGate)
    {
        // The delay is needed to give time for the IRC services to give privileges to the user
        QTimer* timer = new QTimer;
        timer->setSingleShot(true);
        timer->setInterval(VOICEGATE_SEND_INVITE_DELAY*1000);
        connect (timer, &QTimer::timeout, this, [&, timer, u, c](){
            giveTheVoiceFlagIfItsWillBeOkOrSendInvite(u->GetNick(), c);
            timer->deleteLater();
        });
        timer->start();
    }
}

void IRCNetworkClient::Slot_onMode(libircclient::Parser *p)
{
    QStringList params = p->GetParameters();
    if (params.size() != 3) // "#testroom", "+o", "IRCaBot"
    {
        return;
    }

    static const QRegularExpression moderPriv("^\\+[^v]");

    if (not params.at(1).contains(moderPriv))
    {
        return;
    }
    else if (params.at(2) != m_network->GetLocalUserInfo()->GetNick())
    {
        return;
    }

    giveTheVoiceFlag(m_voicedList.nicks(), params.at(0));
}

void IRCNetworkClient::Slot_onWelcome()
{
    LOG_INFO << "Connected to server";
    if (m_sleepPeriod != 1)
    {
        m_sleepPeriod = 1; // drop reconnection timeout to default (1 sec)
    }
    foreach(const auto& channel, m_config->channels)
    {
        m_network->RequestJoin(channel);
    }
}

void IRCNetworkClient::Slot_onDisconnected()
{
    LOG_INFO << "Disconnected from server. Auto reconnect" << (m_config->autoReconnect ? "enabled" : "disabled") + QString(".");
    if (m_config->autoReconnect)
    {
        QThread::sleep(m_sleepPeriod >= 20 ? m_sleepPeriod : m_sleepPeriod++);
        m_network->Reconnect();
    }
}

void IRCNetworkClient::stopPrivate()
{
    connect (m_network.data(), &libircclient::Network::Event_Disconnected, m_network.data(), &libircclient::Network::stop);
    connect (m_network.data(), &libircclient::Network::Event_stopped, this, &IRCNetworkClient::stopped);

    m_config->autoReconnect = false;
    m_network->Disconnect("Shutdowned");
}

void IRCNetworkClient::processVoiceRequest(const QString& sender)
{
    QMutexLocker lock (&m_mtxvoiceGateRequestsTimestamp);

    const auto currTime = currentTimestampSecs();
    if (m_voiceGateRequestsTimestamp.find(sender) != m_voiceGateRequestsTimestamp.end())
    {
        const qint64 lastRequestSecsAgo = currTime - m_voiceGateRequestsTimestamp[sender];
        if (lastRequestSecsAgo < VOICEGATE_REQUEST_MINIMAL_INTERVAL)
        {
            if (lastRequestSecsAgo > VOICEGATE_REQUEST_WITHOUT_ANSWER_THRESHOLD)
            {
                m_network->SendMessage("Too many requests, try again later.", sender, libircclient::Priority_Low);
            }
            LOG_INFO << "Request to voice rejected for" << sender << "by minimal interval" << VOICEGATE_REQUEST_MINIMAL_INTERVAL << "secs";
            return;
        }
    }

    m_voiceGateRequestsTimestamp[sender] = currTime;

    QList<QMap<QString, qint64>::Iterator> oldToRemove;
    for (auto iter = m_voiceGateRequestsTimestamp.begin(); iter != m_voiceGateRequestsTimestamp.end(); iter++)
    {
        if (currTime - iter.value() > VOICEGATE_REQUEST_MINIMAL_INTERVAL)
        {
            oldToRemove.push_back(iter);
        }
    }
    foreach (auto iter, oldToRemove)
    {
        m_voiceGateRequestsTimestamp.erase(iter);
    }

    if (not m_config->voiceGate)
    {
        m_network->SendMessage("Voice gate disabled.", sender);
        return;
    }

    if (m_voicedList.nicks().contains(sender.toLower()))
    {
        m_network->SendMessage("You are already have a voice flag.", sender);
        return;
    }

    sendVoiceGateInvite(sender);
}

void IRCNetworkClient::sendVoiceGateInvite(const QString &nickname)
{
    const QString invite = VoiceGate::getInviteToken( {m_config->networkName, nickname} );
    QString message = "Solve the captcha to get a voice in channels where I have privileges: ";

    QStringListIterator urlsIter(Config::webUiBaseUrls());
    while (urlsIter.hasNext())
    {
        message += urlsIter.next() + QString(HTTP_URI_VOICEGATE_PREFIX) + "/" + QString(HTTP_URI_VOICEGATE_ACTION_INVITE) + "?token=" + invite;
        if (urlsIter.hasNext()) message += " or ";
    }

    m_network->SendMessage(message, nickname);
    m_network->SendMessage("If the link has expired or in any other case, you can request a voice flag via this chat.", nickname);
}

void IRCNetworkClient::Slot_adminIdentified(libircclient::Parser* p)
{
    auto params = p->GetParameters();
    if (params.size() < 2)
    {
        LOG_WARN << "Event_WhoisRegNick failure: params.size() < 2, expected 2";
        return;
    }
    if (p->GetParameters().at(1) != m_config->ircabotAdmin)
    {
        LOG_WARN << "Event_WhoisRegNick failure: requested, but admin changed";
        return;
    }

    AdminIRCPanel::identifiedTimestampUpdate(m_config->networkName);

    processAdminCommand(m_adminCommandBuffer);
    m_adminCommandBuffer.clear();
}

void IRCNetworkClient::processAdminCommand(const QString &text)
{
    bool voiceGateShouldReload = false;
    QStringList answer;
    if (not AdminIRCPanel::parse(m_config->networkName, text, answer, m_voicedList, voiceGateShouldReload, m_config))
    {
        if (not m_adminCommandBuffer.isEmpty())
        {
            m_network->SendMessage("Your authorization is being verified. "
                                   "Try repeating the request after a couple of seconds. "
                                   "If your nickname is not registered, admin feature is not available.", m_config->ircabotAdmin);
        }
        m_adminCommandBuffer = text;
        m_network->RequestWhois(m_config->ircabotAdmin);
        return;
    }

    AdminIRCPanel::identifiedTimestampUpdate(m_config->networkName);
    LOG_INFO << "Admin is doing something:" << text;

    QStringListIterator answerIter (answer);
    while (answerIter.hasNext())
    {
        m_network->SendMessage(answerIter.next(), m_config->ircabotAdmin, libircclient::Priority_RealTime);
        if (answerIter.hasNext()) QThread::msleep(100);
    }

    if (voiceGateShouldReload)
    {
        giveTheVoiceFlag(m_voicedList.nicks());
    }
}

QList<libircclient::Channel *> IRCNetworkClient::getChannelsWhereIAmHavePrivileges()
{
    QList<libircclient::Channel*> result;
    if (not m_config->voiceGate)
    {
        LOG_DEBUG << "no moderated channels, because voice gate disabled for this network";
        return result;
    }

    QMutexLocker lock (&m_mtxNetworkChannelsIteration);

    const auto allChannels = m_network->GetChannels();
    QListIterator<libircclient::Channel*> allChanIter(allChannels);

    while (allChanIter.hasNext())
    {
        auto channel = allChanIter.next();
        auto iAmAtChannel = channel->GetUser(m_network->GetLocalUserInfo()->GetNick());
        if (iAmAtChannel == nullptr)
        {
            LOG_WARN << "IRCNetworkClient::getChannelsWhereIAmHavePrivileges anomaly: can't get myself from" << channel->GetName();
            continue;
        }

        auto highestCUMode = iAmAtChannel->GetHighestCUMode();
        auto iHavePrivileges = highestCUMode != 0 and highestCUMode != 'v' /*simple voice flag, not a moder*/;
        if (not iHavePrivileges)
        {
            continue;
        }

        result.push_back(channel);
    }

    return result;
}

void IRCNetworkClient::addNicknameToVoicedGroup(const QString &nickname)
{
    m_network->SendNotice("You are voiced!"/* The voice flag will be removed if you remain silent for " + QString::number(VOICEFLAG_TIMEOUT/60/60) + " hours."*/, nickname);

    m_voicedList.add(nickname);
    giveTheVoiceFlag(QStringList() << nickname);
}

void IRCNetworkClient::giveTheVoiceFlag(const QStringList& nicknames, const QString& channelName)
{
    auto channels = getChannelsWhereIAmHavePrivileges();
                 // ^ also have this mutex, so should lock this after

    if (channels.isEmpty()) return;

    if (not channelName.isEmpty())
    {
        libircclient::Channel* channelPtr = nullptr;
        QListIterator<libircclient::Channel*> chanIter(channels);
        while (chanIter.hasNext())
        {
            auto channel = chanIter.next();
            if (channel->GetName() == channelName)
            {
                channelPtr = channel;
                break;
            }
        }

        if (channelPtr == nullptr) return;

        channels.clear();
        channels.push_back(channelPtr);
    }

    QMutexLocker lock (&m_mtxNetworkChannelsIteration);

    QListIterator<libircclient::Channel*> chanIter(channels);
    while (chanIter.hasNext())
    {
        auto channel = chanIter.next();
        auto nicknamesAtChannel = channel->GetUsers();
        QHashIterator<QString, libircclient::User*> nicksAtChanIter(nicknamesAtChannel);

        while (nicksAtChanIter.hasNext())
        {
            auto someUserAtChannel = nicksAtChanIter.next();
            if (nicknames.contains(someUserAtChannel.key(), Qt::CaseInsensitive))
            {
                if (someUserAtChannel.value()->GetHighestCUMode() != 0) continue; // already have voice or higher privs
                m_network->TransferRaw("MODE " + channel->GetName() + " +v " + nicksAtChanIter.key());
                LOG_DEBUG << "set +v for" << nicksAtChanIter.key() << "at" << channel->GetName();

                if (nicksAtChanIter.hasNext()) QThread::msleep(100);
            }
        }
    }
}

void IRCNetworkClient::giveTheVoiceFlagIfItsWillBeOkOrSendInvite(const QString &nickname, libircclient::Channel* channel)
{
    auto iAmAtChannel = channel->GetUser(m_network->GetLocalUserInfo()->GetNick());
    if (iAmAtChannel == nullptr)
    {
        LOG_WARN << "IRCNetworkClient::giveTheVoiceFlagIfItsWillBeOk anomaly: can't get myself from" << channel->GetName();
        return;
    }

    auto highestCUMode = iAmAtChannel->GetHighestCUMode();
    auto iHavePrivileges = highestCUMode != 0 and highestCUMode != 'v' /*simple voice flag, not a moder*/;
    if (not iHavePrivileges)
    {
        return;
    }

    auto user = channel->GetUser(nickname);
    if (user == nullptr)
    {
        LOG_WARN << "IRCNetworkClient::getChannelsWhereIAmHavePrivileges anomaly: can't get existed user" << nickname << "from" << channel->GetName();
        return;
    }

    if (user->GetHighestCUMode() != 0) return; // already have a voice

    if (m_voicedList.nicks().contains(nickname.toLower()))
    {
        m_network->TransferRaw("MODE " + channel->GetName() + " +v " + nickname);
        LOG_INFO << "set +v for" << nickname<< "at" << channel->GetName();
    }
    else if (channel->GetMode().GetIncluding().contains('m'))
    {
        if (not m_voiceGateInviteSendedTimestamp.contains(nickname) or
            currentTimestampSecs() - m_voiceGateInviteSendedTimestamp[nickname] > VOICEGATE_SEND_INVITE_MINIMAL_INTERVAL)
        {
            m_voiceGateInviteSendedTimestamp[nickname] = currentTimestampSecs();
            sendVoiceGateInvite(nickname);
        }
    }
}
